/*! 
 * jquery.ux.calendar - v 1.0.0 - http://jquery-ux.com/
 * Copyright (c) 2010 Michael Helgeson, Three Dub Media
 * Open Source MIT License - http://jquery-ux.com/license 
 */
// Created: 2007-02-14 
// Updated: 2010-03-03
// REQUIRES: jquery 1.4+, jquery.ux

;(function( $ ){ // confine scope

$.ux( "calendar", { // Register the PLUGIN... and public methods...
	expose: "display select", // + enable disable destroy
	defaults: {
		today: null, // the date of reference
		display: null, // the starting display date
		min: null, // date obj or date string or milliseconds
		max: null, // date obj or date string or milliseconds
		months: 1, // how many months to display
		multi: false, // (boolean|number) how many days may be selected
		range: false, // (boolena|number) create a continuous selection
		fixed: false, // date obj or date string or milliseconds
		prevText: "&laquo;",
		nextText: "&raquo;",
		// i18n : Internationalization and Localization
		start: 0, // calendar week start day (0=sunday)
		right2left: false, // inverts the ordering convention 
		monthNames: "January February March April May June July August September October November December".split(" "),
		dayNumbers: "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31".split(" "),
		dayNames: "Su Mo Tu We Th Fr Sa".split(" "),
		// custom classes...
		classroot: "ux-calendar"
		},
	create: function( elem, opts ){
		// store args
		$.extend( this, { elem:elem, opts:opts }); 
		// create the main container/controller...
		this.$container = $('<a href="#'+ this.uxid +'" class="'+ this.classify("wrap") +'"/>').appendTo( elem )
			.bind( "click mouseover mouseout keypress wheel", $.proxy( this, "handler") );
		// create the prev/next controls...
		this.$prev = $('<div class="'+ this.classify("prev") +'">'+ opts.prevText +'</div>');
		this.$next = $('<div class="'+ this.classify("next") +'">'+ opts.nextText +'</div>');
		// initialize some states
		this.$hovered = $([]); // the hovered cell
		this.$selected = $([]); // the selected cells
		this.selected = {}; // lookup table for all selected dates
		this.stored = []; // array to hold the selected dates in order
		this.today = this.Date( opts.today || new Date() );
		// determine the date max and min limits
		this.max = ( this.Date( opts.max ) || this.Date("Dec 31 9999") ).getTime();
		this.min = ( this.Date( opts.min ) || this.Date("Jan 01 0100") ).getTime();	
		// determine the month and year to display
		this.display( this.Date( opts.display ) || this.today );
		},
	destroy: function(){
		this.$container.unbind("click mouseover mouseout keypress wheel", $.proxy( this, "handler") ).remove().empty();
		},
	methods: {
		// handle various events
		handler: function( event ){
			if ( this.disabled ) return;
			var offset, $target = $( event.target ), $cell = $target.closest("td"),
			data = $cell.closest("div."+ this.classify("month") ).data("ux-calendar");
			switch( event.type ){
				case "click":
					// skip invalid dates
					if ( $cell.is("."+ this.classify("invalid") ) )
						break;
					// current month
					if ( $cell.is("."+ this.classify("thismonth") ) )
						this.select( ( data.month + 1 ) +'/'+ this.number('day', $cell ) +'/'+ data.year );
					// previous month
					if ( $target[0] == this.$prev[0] ) 
						this.display( this.prev, true );
					// next month
					if ( $target[0] == this.$next[0] ) 
						this.display( this.next, true );
					break;
				case "wheel": // event.wheel (mousewheel)
					this.display( this[ event.delta > 0 ? "prev" : "next" ], true );
					break;
				case "mouseout": 
					this.hover();
					break;
				case "mouseover":
					this.hover( $cell.is("tbody td") ? $cell : null );
					break;
				case "keypress":
					switch ( event.keyCode ){
						case 33: // PAGEUP
							this.display( event.shiftKey ? this.prev_year : this.prev ); 
							break;
						case 34: // PAGEDOWN
							this.display( event.shiftKey ? this.next_year : this.next );
							break;
						case 38: // UP
							offset = -6048e5;
							break;
						case 40: // DOWN
							offset = 6048e5;
							break;
						case 37: // LEFT
							offset = -864e5;
							break;
						case 39: // RIGHT
							offset = 864e5;
							break;
						case 36: // HOME
							this.display( this.today );
							this.select( this.today.getTime(), true ); 
							break;
						case 13: // ENTER
							this.select( this.number('ms', this.$hovered ) );
						default: 
							return;
						}
					if ( offset ){ 
						$cell = this.find( this.number('ms', this.$hovered ) + offset );
						if ( $cell.length ) this.hover( $cell );
						}
					break;
				default: return;	
				}	
			event.preventDefault();
			},
		// hilight the given cell
		hover: function( $cell ){
			this.$hovered.removeClass( this.classify("hovered") );
			if ( $cell ) 
				this.$hovered = $cell.addClass( this.classify("hovered") );
			},
		// select or unselect a given date
		select: function( date, bool ){ 
			// check for deselect...
			if ( date === false ){
				this.flush().start = this.finish = null
				return;
				}
			// set the display first...
			if ( this.display( date ) === false ) 
				return;
			date = this.Date( date );
			// some local refs...	
			var ms = date.getTime(), temp, 
			fixed = this.opts.fixed ? this.Date( this.opts.fixed ).getTime() : null,
			multi = 864e5 * ( parseInt( this.opts.multi ) - 1 ),
			range = 864e5 * ( parseInt( this.opts.range ) - 1 );
			// validate range
			// if (  ms < this.min || ms > this.max ) return; 
			ms = Math.max( Math.min( ms, this.max ), this.min );
			// determine the (optional) boolean setting
			bool = !!( bool == null ? !this.selected[ ms ] : bool );
			// SINGLE SELECT 
			if ( !this.opts.multi || this.opts.multi === 1 ) this.flush().remember( ms );
			// RANGE SELECTION
			else if ( this.opts.range ){					
				// determine the date to walk away from...				
				temp = fixed || this.stored.pop() || ms; // use last selected date
				// use oldest or newest date
				if ( this.start != null && this.finish != null )
					temp = ms > this.finish ? this.start : ms < this.start ? this.finish : temp; 
				// apply a fixed range...
				if ( !isNaN( range ) ) 
					temp = ms <= temp ? ms + range : ms - range;
				// apply the range limitation...
				else if ( !isNaN( multi ) && Math.abs( ms - temp ) > multi ) 
					temp = ms < temp ? ms + multi : ms - multi;
				// clear everything, store the range endpoints
				this.flush().remember( temp ).remember( ms );
				// copy and sort the array by date
				temp = this.stored.slice().sort();
				// determine the range start date
				this.start = temp[ 0 ];
				// determine the range end date	
				this.finish = temp.pop();
				// highlight the visible range selection...
				this.range();
				}
			// MULTI SELECT	
			else this[ bool ? "remember" : "forget" ]( ms );
			this.publish("select", this.collect() ); // "calendar:select"
			},
		// prepare the selected dates for callback argument
		collect: function(){
			var array = this.stored.slice(); // clone array 
			if ( this.opts.range ) // range select
				array.sort(); // order [ start, end ]
			if ( !this.opts.multi || this.opts.multi === 1 ) // single select
				array = array[0]; // remove array
			return array;
			},
		// highlight the visible range
		range: function(){
			var $range = $([]), $week, cells = "td."+ this.classify("thismonth");
			if ( this.opts.right2left ) // right to left
				this.$container.find("tbody tr").each(function(){ // each week
					$week = $( this ).find( cells ); // left to right
					Array.prototype.reverse.call( $week ); // invert order
					$range = $range.add( $week ); // merge results
					});
			else $range = this.$container.find( cells ); // left to right
			return $range.slice(
				$range.index( this.find( Math.max( this.start, this.begin ) ) ),
				$range.index( this.find( Math.min( this.finish, this.end ) ) ) + 1
				).addClass( this.classify("selected") ); 
			},
		// remove all selected dates
		flush: function(){
			this.$container.find("td."+ this.classify("selected") )
				.removeClass( this.classify("selected") ); // unstyle ALL
			this.selected = {}; // clear ALL stored data
			this.stored = []; // reset the cache
			return this;
			},
		// store the date as selected
		remember: function( ms ){
			if ( this.stored.length == parseInt( this.opts.multi ) ) // limit selection...
				this.forget( this.stored[0] ); // remove oldest value...
			this.selected[ ms ] = true; // store this dates CSS query selector
			this.stored.push( ms ); // reset the cache
			this.hover( this.find( ms ).addClass( this.classify("selected") ) );
			return this;
			},
		// remove the date from selected
		forget: function( ms ){
			delete this.selected[ ms ];
			this.stored = $.grep( this.stored, function( val ){ 
				return val != ms; // filter the array
				});
			this.hover( this.find( ms ).removeClass( this.classify("selected") ) );
			return this;
			},	
		// find the cell for the given date
		find: function( ms ){
			// check to see if it is in the current display range...
			if (  ms < this.begin || ms > this.end ) return $([]);
			// query the DOM for the matching date...
			return this.$container.find("td.ms"+ ms );
			},
		// extract the date number from classname
		number: function( word, $node ){		
			try { 
				word = new RegExp("(?:\\s|^)"+ word +"(\\d+)(?:\\s|$)").exec( $node[0].className )[1];
				}
			catch ( ex ){ 
				word = undefined; 
				}
			return parseInt( word );
			},
		// show calendars starting with the month with the given date
		display: function( date, forced ){
			if ( this.disabled || !( date = this.Date( date ) ) ) return false;
			// if date is visible, do nothing
			if ( !forced && date >= this.begin && date <= this.end ) return;
			// limit the display range to the min/max
			date = this.Date( Math.max( Math.min( date.getTime(), this.max ), this.min ) );
			// do all the months...
			var i, y = date.getFullYear(), m = date.getMonth(), $divs = $([]);
			for ( i = 0; i< this.opts.months; i++ )
				$divs = $divs.add( this.render( m+i, y ) );
			// determine the display limits
			this.begin = this.Date( y, m, 1 ).getTime();	
			this.end = this.Date( y, m + i, 0 ).getTime();
			// determine prev/next month dates
			this.prev = this.Date( y, m - 1, 1 ).getTime();
			this.next = this.Date( y, m + 1, 1 ).getTime();
			// determine prev/next year dates
			this.prev_year = this.Date( y - 1, m, 1 ).getTime();
			this.next_year = this.Date( y + 1, m, 1 ).getTime();
			// replace the entire content		
			//$divs.appendTo( this.$container.empty() );
			this.$container.empty().append( $divs );
			// re-insert prev/next controls
			if ( this.begin > this.min ) 
				// this.$container.prepend( this.$prev );
				$divs.eq( 0 ).prepend( this.$prev );
			if ( this.end < this.max ) 
				//this.$container.append( this.$next );
				$divs.eq( $divs.length-1 ).prepend( this.$next );
			// publish callback
			this.publish("update"); // "calendar:update"
			},
		// create the actual DOM object(s) for the month with the given date
		render: function( m, y ){
			// reference the config options...
			var opts = this.opts, 
			// find the first day of the displayed month
			first = this.Date( y, m, 1 ), begin = first.getDay(),
			// normalize the args (in case of rollover)
			month = first.getMonth(), year = first.getFullYear(),
			// determine the end of the displayed month...
			length = 32 - ( new Date( year, month, 32 ).getDate() ),
			today = this.today.getTime(),
			// more local variables...
			start = opts.start, count = start, temp,
			week, day, num, date, cls, time, html = [];
			// adjust the starting day...
			begin += begin < start ? 7 : 0;
			// begin the month container...
			html.push('<div class="year', year, ' ', this.classify("month"), '">',
				'<table cellpadding="0" cellspacing="0" border="0" ',
				'class="month', ( month + 1 ), ' ', this.classify("table")  ,'">');
			// the month table header...
			html.push('<thead><tr><td colspan="7" class="', this.classify("title")  ,'">',
				this.opts.monthNames[ month%12 ], " ", year,
				'</td></tr><tr class="', this.classify("weekdays"), '">' );
			temp = []; // a buffer array to hold days
			for ( day = start; day < start+7; day++ )
				temp.push('<td class="weekday'+( day%7 )+'">'+ this.opts.dayNames[ day%7 ] +'</td>');
				
			if ( opts.right2left ) temp.reverse(); // invert direction	
			html.push.apply( html, temp ); // flush the buffer
			html.push('</tr></thead>','<tbody>');
			// the calendar body...
			for ( week = 0; week < 6; week++ ){ // six weeks
				html.push('<tr>');
				temp = []; // buffer
				for ( day = start; day < start+7; day++ ){ // seven days
					num = ( ++count - begin ); // offset the month start index
					date = this.Date( year, month, num );
					time = date.getTime();
					// build up some classnames to style this date cell...
					cls = [ "weekday"+( day%7 ) ]; // day of the week class...	
					if ( num > length ) cls.push( this.classify("nextmonth") );
					else if ( num < 1 ) cls.push( this.classify("prevmonth") );
					else { // date is inside the current month
						cls.push( this.classify("thismonth"), "day"+ num, "ms"+time );
						if ( this.selected[ time ] || ( this.opts.range && time >= this.start && time <= this.finish ) )
							cls.push( this.classify("selected") ); // selected date
						}
					if ( today == time ) cls.push( this.classify("today") ); // today's date	
					if ( time < this.min || time > this.max ) cls.push( this.classify("invalid") ); // over/under min/max
					// create the actual cell...
					temp.push('<td class="'+ cls.join(" ")+'">'+ this.opts.dayNumbers[ date.getDate()-1 ] +'</td>');
					}
				if ( opts.right2left ) temp.reverse(); // invert direction	
				html.push.apply( html, temp ); // flush the buffer
				html.push('</tr>');
				}
			// finish the calendar	
			html.push('</tbody>','</table></div>');
			// Create elements from the concatenated HTML...
			return $( html.join("") ).data("ux-calendar",{ month:month, year:year });
			},
		// create a date object, from varied input, with standard time
		Date: function( year, month, day ){ var date;
			if ( year == null ) return false; // bad arg
			if ( arguments.length < 3 ) { // string, milliseconds, date object
				date = isDate( year ) ? year : new Date( year );
				year = date.getFullYear(); 
				month = date.getMonth();
				day = date.getDate();
				}
			date = new Date( year, month, day, 12, 0, 0, 0 ); // noon
			return isDate( date ) ? date : false;
			}
		}
	});

// is arg a valid date object
function isDate( arg ){
	return !!arg && arg.constructor == Date && !isNaN( arg.getTime() ); 
	};

})( jQuery ); // secure the $ jQuery alias